/**
 * This file is part of Lineage II Client Data Parser.
 * 
 * Lineage II Client Data Parser is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * Lineage II Client Data Parser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with Lineage II Client Data Parser.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.l2emuproject.io;

import java.io.BufferedOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import net.l2emuproject.redist.CustomLogger;

import javolution.text.TextBuilder;

/**
 * @author Savormix
 *
 */
public final class SafelyPackedFile implements DataInput
{
	private static final CustomLogger	log			= CustomLogger.getLog();
	private static final String			FILE_TYPE	= "SafePackage";
	private static final String			LENGTHLESS	= "";

	private final String				filename;
	private final ByteBuffer			wrapper;
	private final int					chronicle;
	private int							dumpOffset;

	/**
	 * Creates a class for easy file reading
	 * @param filename File name
	 * @param data byte array containing the whole file data
	 * @param chronicle Chronicle ID
	 */
	public SafelyPackedFile(String filename, ByteBuffer data, int chronicle)
	{
		this.filename = filename;
		this.wrapper = data;
		this.chronicle = chronicle;
		wrapper.order(ByteOrder.LITTLE_ENDIAN);
		dumpOffset = 0;
		if (testPackage())
			wrapper.limit(wrapper.limit() - FILE_TYPE.length() - 2);
		else
			log.warn("Incorrect input file: " + filename, new Exception());
	}

	/**
	 * @return whether the file is correctly packed
	 */
	public boolean testPackage()
	{
		wrapper.position(wrapper.limit() - FILE_TYPE.length() - 2);
		boolean test = readString().equals(FILE_TYPE);
		wrapper.rewind();
		log.debug(this + " package status " + test);
		return test;
	}

	/**
	 * Moves the active pointer given number of bytes forward (backward if offset is
	 * negative).
	 * @param offset Number of bytes to move the pointer
	 */
	public void movePointer(int offset)
	{
		wrapper.position(wrapper.position() + offset);
		//dumpOffset += offset;
	}

	/** Moves the active pointer to the beginning of the content byte array  */
	public void resetPointer()
	{
		wrapper.rewind();
		log.debug("Offset was reset.");
		resetReadStream();
	}

	/**
	 * Determines if name/description exist/is a malformed String
	 * @return true if next four bytes indicate a readable non-empty String, false
	 * otherwise
	 */
	public boolean canRead()
	{
		int a, b, c, d;
		boolean special = false;
		movePointer(-2);
		a = readByte();
		b = readByte();
		c = readByte();
		d = readByte();
		special = (a == 32 && b == 0 && c == 0);
		return (c != 0 || d != 0) && !special;
	}

	/** @return file name this class instance represents */
	public String getFileName()
	{
		return filename;
	}

	/**
	 * @return the chronicle ID
	 */
	public int getChronicleID()
	{
		return chronicle;
	}

	/** Used for debugging/testing/implementation purposes only */
	public void resetReadStream()
	{
		dumpOffset = wrapper.position();
		log.debug("Read data stream was reset.");
	}

	/**
	 * Used for debugging/testing/implementation purposes only
	 * @param reset delete stream contents
	 */
	public void dumpReadStream(boolean reset)
	{
		File f = new File("debug", FileManager.SP_READ_OUTPUT);
		try
		{
			f.createNewFile();
			OutputStream out = new BufferedOutputStream(new FileOutputStream(f, false));
			out.write(wrapper.array(), dumpOffset, wrapper.position());
			out.close();
		}
		catch (IOException e)
		{
			log.debug("Could not dump read data!", e);
		}
		finally
		{
			if (reset)
				resetReadStream();
		}
	}

	/**
	 * @return a default SafePackage string, where the first byte indicates it's length.
	 */
	public String readString()
	{
		return readString(readUnsignedByte());
	}

	/**
	 * @param len predicted string length
	 * @return a default SafePackage string
	 */
	public String readString(int len)
	{
		if (len == 0)
			return LENGTHLESS;
		byte[] buf = new byte[len];
		int unk = readUnsignedByte();
		switch (unk)
		{
			case 1:
			case 2:
			case 3:
			case 7:
			case 8:
				break;
			default:
				movePointer(-1);
		}
		int utf16 = readUnsignedShort(), test = readUnsignedShort();
		movePointer(-4);
		if (utf16 < 256 && test < 256)
			return readUTF16();
		readFully(buf);
		String s = new String(buf);
		if (!s.endsWith("\0"))
		{
			log.debug("bad string: " + s);
			int left = 1;
			while (readByte() != 0)
				left++;
			movePointer(-left);
			s += readString(left);
			log.debug("good string: " + s);
		}
		return s.replace("\0", "");
	}

	/**
	 * @return UTF16 encoded string
	 */
	public String readUTF16()
	{
		TextBuilder tb = TextBuilder.newInstance();
		char c;
		while ((c = readChar()) != 0)
			tb.append(c);
		String s = tb.toString();
		TextBuilder.recycle(tb);
		return s;
	}

	/**
	 * @param byteCount byte count
	 * @return a string that represents the char array
	 */
	public String readPreciseString(int byteCount)
	{
		TextBuilder tb = TextBuilder.newInstance();
		for (int i = 0; i < byteCount / 2; i++)
			tb.append(readChar());
		String s = tb.toString();
		TextBuilder.recycle(tb);
		return s;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString()
	{
		return "SafePackage: " + getFileName();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readBoolean()
	 */
	@Override
	public boolean readBoolean()
	{
		return readByte() != 0;
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readByte()
	 */
	@Override
	public byte readByte()
	{
		return wrapper.get();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readChar()
	 */
	@Override
	public char readChar()
	{
		return wrapper.getChar();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readDouble()
	 */
	@Override
	public double readDouble()
	{
		return wrapper.getDouble();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readFloat()
	 */
	@Override
	public float readFloat()
	{
		return wrapper.getFloat();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readFully(byte[])
	 */
	@Override
	public void readFully(byte[] b)
	{
		try
		{
			wrapper.get(b);
		}
		catch (BufferUnderflowException e)
		{
			log.debug("Cannot read next " + b.length + " bytes! Dumping read data...");
			dumpReadStream(false);
			log.debug("Finished, exiting...");
			System.exit(-1);
		}
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readFully(byte[], int, int)
	 */
	@Override
	public void readFully(byte[] b, int off, int len)
	{
		wrapper.get(b, off, len);
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readInt()
	 */
	@Override
	public int readInt()
	{
		return wrapper.getInt();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readLine()
	 */
	@Override
	@Deprecated
	public String readLine()
	{
		return null;
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readLong()
	 */
	@Override
	public long readLong()
	{
		return wrapper.getLong();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readShort()
	 */
	@Override
	public short readShort()
	{
		return wrapper.getShort();
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readUTF()
	 */
	@Override
	public String readUTF()
	{
		try
		{
			return DataInputStream.readUTF(this);
		}
		catch (IOException e)
		{
			return null;
		}
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readUnsignedByte()
	 */
	@Override
	public int readUnsignedByte()
	{
		return wrapper.get() & 0xff;
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#readUnsignedShort()
	 */
	@Override
	public int readUnsignedShort()
	{
		int a = wrapper.get();
		int b = wrapper.get();
		return (((b & 0xff) << 8) | (a & 0xff));
	}

	/* (non-Javadoc)
	 * @see java.io.DataInput#skipBytes(int)
	 */
	@Override
	@Deprecated
	public int skipBytes(int n)
	{
		return 0;
	}
}
